React diff揭秘

React diff揭秘

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

image-20230506113307060

ReactChildFiber.new.js中

Diff的入口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 根据newChild类型选择不同diff函数处理
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
): Fiber | null {

const isObject = typeof newChild === 'object' && newChild !== null;

if (isObject) {
// object类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 调用 reconcileSingleElement 处理

case REACT_PORTAL_TYPE:
// 调用 reconcileSinglePortal
return placeSingleChild(
reconcileSinglePortal(
... //四个参数
),
);
}
}

if (typeof newChild === 'string' || typeof newChild === 'number') {
// 调用 reconcileSingleTextNode 处理
reconcileSingleTextNode
}

if (isArray(newChild)) {
// 调用 reconcileChildrenArray 处理
reconcileChildrenArray
}

// 一些其他情况调用处理函数

// 以上都没有命中,删除节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

newChild参数就是本次更新的 JSX 对象(对应ClassComponentthis.render方法返回值,或者FunctionComponent执行的返回值)


同级的节点数量将Diff分为两类:

  1. 当newChild类型为objectnumberstring,代表同级只有一个节点
  2. 当newChild类型为Array时,代表同级有多个节点

情况一:同级只有一个节点

对于单个节点,我们以类型object为例,会进入reconcileSingleElement

1
2
3
4
5
6
7
8
9
10
const isObject = typeof newChild === 'object' && newChild !== null;

if (isObject) {
// 对象类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
// 调用 reconcileSingleElement 处理
// ...其他case
}
}

image-20230506113338514

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

1
2
3
4
5
6
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}

image-20230506113412870

render阶段会生成的Fiber结构

  • Fiber中可以保存节点的类型,例子中App节点是一个函数组件节点,div节点是一个原生DOM节点,I am节点是一个文本节点。

  • 可以保存节点的信息(比如state,props)。

  • 可以保存节点对应的值(比如App节点对应App函数,div节点对应div DOMElement)。这样的结构也解释了为什么函数组件通过Hooks可以保存state。因为state并不是保存在函数上,而是保存在函数组件对应的Fiber节点上。

  • 可以保存节点的行为(更新/删除/插入)

image-20230506113425520

div Fiber.return = App Fiber; 即用return指向自己的父节点。父级叫return不叫parent

image-20230506113435102

image-20230506113445318

判断DOM节点是否可以复用,让我们通过代码看看是如何判断的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement
): Fiber {
const key = element.key;
let child = currentFirstChild;

// 首先判断是否存在对应DOM节点
while (child !== null) {
// 上一次更新存在DOM节点,接下来判断是否可复用
if (child.key === key) {

// 🙋‍♂️同学看这里,首先比较key是否相同
switch (child.tag) {
// ...省略case

default: {
if (child.elementType === element.type) {
// 🙋‍♂️同学看这里,key相同后再看type是否相同
// 如果相同则表示可以复用
return existing;
}

// type不同则跳出循环
break;
}
}
// 👹 key不同或type不同都代表不能复用,会到这里
// 不能复用的节点,被标记为删除
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}

// 创建新Fiber,并返回
}

从代码可以看出,React通过先判断key是否相同,如果key相同则判断type是否相同,只有都相同时一个DOM节点才能复用。

练习题

请判断如下JSX对象对应的DOM元素是否可以复用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 习题1 更新前
<div>123</div>
// 更新后
<p>123</p>

// 习题2 更新前
<div key="xxx">123</div>
// 更新后
<div key="ooo">123</div>

// 习题3 更新前
<div key="xxx">123</div>
// 更新后
<p key="ooo">123</p>

// 习题4 更新前
<div key="xxx">123</div>
// 更新后
<div key="xxx">456</div>

`

`

`

`

`

`

`

习题1: 未设置key prop默认 key = null;,所以更新前后key相同,都为null,但是更新前type为div,更新后为p,type改变则不能复用。

习题2: 更新前后key改变,不需要再判断type,不能复用。

习题3: 更新前后key改变,不需要再判断type,不能复用。

习题4: 更新前后key与type都未改变,可以复用。children变化,DOM的子元素需要更新。

情况二:同级有多个元素的Diff

刚才我们介绍了单一元素的Diff,现在考虑我们有一个FunctionComponent

1
2
3
4
5
6
7
8
9
10
function List () {
return (
<ul>
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
<li key="3">3</li>
</ul>
)
}

返回值JSX对象的children属性不是单一元素,而是包含四个对象的数组

image-20230506113503250

在这种情况下,reconcileChildFibers的newChild参数为Array,就执行到了这儿

image-20230506113511965

来看看React如何处理同级多个元素的Diff。

同级多个节点详解

  1. 节点更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 情况1 节点更新

    // 之前
    <ul>
    <li key="0" className="before">0<li>
    <li key="1">1<li>
    </ul>

    // 之后情况1 节点属性变化
    <ul>
    <li key="0" className="after">0<li>
    <li key="1">1<li>
    </ul>

    // 之后情况2 节点类型更新
    <ul>
    <div key="0">0<li>
    <li key="1">1<li>
    </ul>
  1. 节点新增或减少

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 情况2 节点新增或减少

    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后情况1 新增节点
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    <li key="2">2<li>
    </ul>

    // 之后情况2 删除节点
    <ul>
    <li key="1">1<li>
    </ul>
  1. 节点位置变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 情况3 节点位置变化

    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="1">1<li>
    <li key="0">0<li>
    </ul>

同一次同级多个元素的Diff,一定属于以上三种情况中的一种或多种。

该如何设计算法呢

首先想到的是方案是:

  1. 判断当前节点的更新属于哪种情况
  2. 如果是新增,执行新增逻辑
  3. 如果是删除,执行删除逻辑
  4. 如果是更新,执行更新逻辑

按这个方案,其实有个隐含的前提,上述不同操作的优先级是相同的

但React团队发现,在日常开发中,相对于增加和删除,更新组件发生的频率更高。所以React Diff会优先判断当前节点是否属于更新。

虽然本次更新的JSX对象newChildren为数组形式,但是和newChildren中每个值进行比较的是上次更新的Fiber节点,Fiber节点的同级节点是由sibling(兄弟节点)指针链接形成的链表。

即 newChildren[0]与oldFiber比较,newChildren[1]与oldFiber.sibling比较。

单链表无法使用双指针,所以无法对算法使用双指针优化。

基于以上原因,Diff算法的整体逻辑会经历两轮遍历。

第一轮遍历:处理更新的节点。

第二轮遍历:处理剩下的不属于更新的节点。

第一遍遍历

  1. 遍历newChildren,i = 0,将newChildren[i]与oldFiber比较,判断DOM节点是否可复用。
  2. 如果可复用,i++,比较newChildren[i]与oldFiber.sibling是否可复用。可以复用则重复步骤2。
  3. 如果不可复用,立即跳出整个遍历。
  4. 如果newChildren遍历完或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历。

当最终完成遍历后,会有两种结果:

结果一:如果是步骤3跳出的遍历,newChildren没有遍历完,oldFiber也没有遍历完。

举个栗子🌰

如下代码中,前2个节点可复用,key === 2的节点type改变,不可复用,跳出遍历。

此时oldFiber剩下key === 2未遍历,newChildren剩下key === 2、key === 3未遍历。

1
2
3
4
5
6
7
8
9
10
// 之前
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>

// 之后
<li key="0">0</li>
<li key="1">1</li>
<div key="2">2</div>
<li key="3">3</li>

结果二:如果是步骤4跳出的遍历,可能newChildren遍历完,或oldFiber遍历完,或他们同时遍历完。

再来个🌰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 之前
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>


// 之后情况1 newChildren与oldFiber都遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>


// 之后情况2 newChildren没遍历完,oldFiber遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>


// 之后情况3 newChildren遍历完,oldFiber没遍历完
<li key="0" className="aa">0</li>

第二轮遍历

对于结果二,聪明的你想一想🐯,newChildren没遍历完,oldFiber遍历完意味着什么?

老的DOM节点都复用了,这时还有新加入的节点,意味着本次更新有新节点插入,我们只需要遍历剩下的newChildren依次执行插入操作(Fiber.effectTag = Placement;)

effectTag字段表示当前Fiber需要执行的副作用,最常见的副作用是:

  • Placement 插入DOM节点
  • Update 更新DOM节点
  • Deletion 删除DOM节点

同样的,我们举一反三。newChildren遍历完,oldFiber没遍历完意味着什么?

意味着多余的oldFiber在这次更新中已经不存在了,所以需要遍历剩下的oldFiber,依次执行删除操作(Fiber.effectTag = Deletion;)。

那么结果一怎么处理呢?newChildren与oldFiber都没遍历完,这意味着有节点在这次更新中改变了位置。

接下来,就是Diff算法最精髓的部分!!!

处理位置交换的节点

为了快速的找到key对应的oldFiber,我们将所有还没处理的oldFiber放进以key属性为key,Fiber为value的map。

1
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

源码部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function mapRemainingChildren(
returnFiber: Fiber,
currentFirstChild: Fiber,
): Map<string | number, Fiber> {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. Implicit (null) keys get added to this set with their index
// instead.
const existingChildren: Map<string | number, Fiber> = new Map();

let existingChild = currentFirstChild;
while (existingChild !== null) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}

再遍历剩余的newChildren,通过newChildren[i].key就能在existingChildren中找到key相同的oldFiber。

接下来是重点哦,敲黑板 👨‍🏫

在我们第一轮和第二轮遍历中,我们遇到的每一个可以复用的节点,一定存在一个代表上一次更新时该节点状态的oldFiber,并且页面上有一个DOM元素与其对应。

那么我们在Diff函数的入口处,定义一个变量

上篇React diff 中的LastIndex

1
let lastPlacedIndex = 0;

该变量表示当前最后一个可复用的节点,对应的oldFiber在上次更新中的所在的位置索引,我们通过这个变量判断节点是否需要移动。

这里我们简化一下书写,每个字母代表一个节点,字母的值代表节点的key

// 之前
abcd

// 之后
acdb

index 节点 oldIndex lastIndex 操作
0 B 0 0 oldIndex(0)==lastIndex(0),不动
1 c 2 2 oldIndex(2)==lastIndex(2),不动
2 d 3 3 oldIndex(3)==lastIndex3),不动
3 b 1 3 oldIndex(1)<lastIndex(3),节点b移动至index(3)的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 之前
abcd

// 之后
acdb


===第一轮遍历开始===
a(之后)vs a(之前)
key不变,可复用
此时 a 对应的oldFiber(之前的a)在之前的数组(abcd)中索引为0
所以 lastPlacedIndex = 0;

继续第一轮遍历...

c(之后)vs b(之前)
key改变,不能复用,跳出第一轮遍历
此时 lastPlacedIndex === 0;
===第一轮遍历结束===



===第二轮遍历开始===

newChildren === cdb,没用完,不需要执行删除旧节点
oldFiber === bcd,没用完,不需要执行插入新节点

将剩余oldFiber(bcd)保存为map



// 当前oldFiber:bcd
// 当前newChildren:cdb

继续遍历剩余newChildren

key === c 在 oldFiber中存在
const oldIndex = c(之前).index;
即 oldIndex 代表当前可复用节点(c)在上一次更新时的位置索引
此时 oldIndex === 2; // 之前节点为 abcd,所以c.index === 2
比较 oldIndex 与 lastPlacedIndex;


如果 oldIndex >= lastPlacedIndex 代表该可复用节点不需要移动
并将 lastPlacedIndex = oldIndex;
如果 oldIndex < lastplacedIndex 该可复用节点之前插入的位置索引小于这次更新需要插入的位置索引,代表该节点需要向右移动


在例子中,oldIndex 2 > lastPlacedIndex 0
则 lastPlacedIndex = 2;
c节点位置不变


继续遍历剩余newChildren

// 当前oldFiber:bd
// 当前newChildren:db



key === d 在 oldFiber中存在
const oldIndex = d(之前).index;

oldIndex 3 > lastPlacedIndex 2 // 之前节点为 abcd,所以d.index === 3
则 lastPlacedIndex = 3;
d节点位置不变


继续遍历剩余newChildren

// 当前oldFiber:b
// 当前newChildren:b

key === b 在 oldFiber中存在

const oldIndex = b(之前).index;

oldIndex 1 < lastPlacedIndex 3 // 之前节点为 abcd,所以b.index === 1
则 b节点需要向右移动
===第二轮遍历结束===


最终acd 3个节点都没有移动,b节点被标记为移动

相信你已经明白了节点移动是如何判断的

再来看一个例子

1
2
// 之前abcd
// 之后dabc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
===第一轮遍历开始===
d(之后)vs a(之前)
key不变,type改变,不能复用,跳出遍历
===第一轮遍历结束===



===第二轮遍历开始===
newChildren === dabc,没用完,不需要执行删除旧节点
oldFiber === abcd,没用完,不需要执行插入新节点

将剩余oldFiber(abcd)保存为map


继续遍历剩余newChildren

// 当前oldFiber:abcd
// 当前newChildren dabc


key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
此时 oldIndex === 3; // 之前节点为 abcd,所以d.index === 3
比较 oldIndex 与 lastPlacedIndex;
oldIndex 3 > lastPlacedIndex 0
则 lastPlacedIndex = 3;
d节点位置不变



继续遍历剩余newChildren
// 当前oldFiber:abc
// 当前newChildren abc

key === a 在 oldFiber中存在
const oldIndex = a(之前).index; // 之前节点为 abcd,所以a.index === 0
此时 oldIndex === 0;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 0 < lastPlacedIndex 3
则 a节点需要向右移动



继续遍历剩余newChildren

// 当前oldFiber:bc
// 当前newChildren bc


key === b 在 oldFiber中存在
const oldIndex = b(之前).index; // 之前节点为 abcd,所以b.index === 1
此时 oldIndex === 1;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 1 < lastPlacedIndex 3
则 b节点需要向右移动



继续遍历剩余newChildren

// 当前oldFiber:c
// 当前newChildren c

key === c 在 oldFiber中存在
const oldIndex = c(之前).index; // 之前节点为 abcd,所以c.index === 2
此时 oldIndex === 2;
比较 oldIndex 与 lastPlacedIndex;
oldIndex 2 < lastPlacedIndex 3
则 c节点需要向右移动


===第二轮遍历结束===

可以看到,我们以为从 abcd 变为 dabc,只需要将d移动到前面。

但实际上React保持d不变,将abc分别移动到了d的后面。

从这点可以看出,考虑性能,我们要尽量减少将节点从后面移动到前面的操作。

注释过的React源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// diff算法会进行两轮遍历,可能中间有中断,时间复杂度O(n)
// 可复用节点的几种情况:
// 1. 相同key(index可以不同)相同type
// 2. 没有key,相同index,相同type
function reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren,
expirationTime // 最新版本换为了 lanes: Lanes,
) {
// 由于fiber没有保存before引用,所以无法通过头尾双指针的方式优化diff算法

// diff完成后新的第一个child
let resultingFirstChild = null;
let previousNewFiber = null;
// 可复用节点的位置可能和上次不同(需要标记Placement代表移动) ex: abcd => badc
// 所以判断完是否可复用后还需要比较index,具体逻辑见 placeChild
let lastPlacedIndex = 0;
// 遍历到的newChild 索引
let newIdx = 0;
// 遍历过程中用于比较的老fiber
let oldFiber = currentFirstChild;
let nextOldFiber = null;

// 第一轮遍历,对比oldFiber与newChildren[i]寻找可以复用的fiber,可复用条件:
// 1. 新旧节点都为文本节点,直接复用(文本节点没有key)
// 2. 其他类型节点判断key是否相同决定复用(可能key相同但是类型不同)
// 这次遍历要求新旧fiber key相同,顺序相同,如果遇到不满足的则跳出这次遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
// fiber.index 始终等于该fiber在数组中的索引,即使其前一个兄弟节点是null
// ex: [null, a] , 其中 a.index === 1
// 上次的索引大于这次,代表上次这个节点之前的兄弟节点有null ex: [null, a]
// 假设这次是 [b, a] ,则实际上 diff的是 null -> b a -> a
// 所以这里这么赋值
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
// key相同则更新fiber
// 更新包括 复用fiber或者创建新fiber
// key不同则返回null,代表该节点不能复用

// 尝试复用节点
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
expirationTime
)
if (newFiber === null) {
// 该索引对应位置的新节点是 null
if (!oldFiber) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && !newFiber.alternate) {
// oldFiber与newFiber都存在代表对应索引key没变化
// !newFiber.alternate代表newFiber是新创建的fiber
// ex:oldFiber: <div key="1"></div> newFiber: <p key="1"></p>
// 新旧fiber key相同,则newFiber存在,但是type不同,所以是创建新fiber,没有对应alternate
// 插入新DOM节点的同时删掉老的DOM节点
deleteChild(returnFiber, oldFiber);
}
}
// 将可复用的新fiber插入,返回插入的索引
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

if (!previousNewFiber) {
// 这是第一个插入的新fiber
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}

// 第二轮遍历情况1 newChildren遍历完时
if (newIdx === newChildren.length) {
// 当newChildren遍历完时,代表第一轮所有新节点都可复用,
// 只需要删除剩下的oldFiber,因为这部分oldFiber在新的数组里已经不存在了
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 第二轮遍历情况2 oldFiber遍历完时
if (!oldFiber) {
// 当oldFiber遍历完时,代表所有oldFiber已经复用完或者这是首次渲染没有oldFiber
// 再遍历newChildren,把新节点append到后面,这部分在oldFiber中不存在的节点是新加入的
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime);
if (!newFiber) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}

// 第二轮遍历情况3 newChildren,oldFiber都未遍历完
// 将可复用的节点移动位置
// 将所有未遍历的oldFiber存入map,这样在接下来的遍历中能O(1)的复杂度就能通过key找到对应的oldFiber
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
expirationTime
);
if (newFiber) {
if (shouldTrackSideEffects) {
if (newFiber.alternate) {
// 存在current,代表我们需要复用这个节点,将对应oldFiber从map中删除
// 这样该oldFiber就不会置为删除
existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}

if (shouldTrackSideEffects) {
// 还留下的oldFiber表示没有被复用,需要删除
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}

// 协调子节点,分为 mount 和 reconcile 2类
// mount用于首次渲染,child没有对应fiber,直接生成fiber,mount不会改变fiber的effectTag,原因见 appendAllChildren
// reconcile用于更新
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, expirationTime) {
// React.createElement类型 或者 子节点是String、Number对应的Array类型
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime
))
}
// 在 beginWork update各类Component时并未处理HostText,这里处理单个HostText
if (typeof newChild === 'number' || typeof newChild === 'string') {
return placeSingleChild(reconcileSingleTextNode(
returnFiber,
currentFirstChild,
newChild,
expirationTime
))
}
// 在 beginWork update各类Component时并未处理HostText,这里处理多个HostText
if (Array.isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime
)
}
}
// 兜底删除
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}

参考大佬

  1. 奇舞周刊. React源码揭秘(三):Diff算法详解

  2. https://github.com/BetaSu/react-on-the-way/blob/master/packages/react-reconciler/ReactChildFiber.js#L265

  3. https://blog.csdn.net/qiwoo_weekly/article/details/106247621

  4. https://juejin.im/post/5e9abf06e51d454702460bf6#heading-5

  5. https://juejin.im/post/5ec507146fb9a047f47cb805 (原作者大佬)

  6. https://segmentfault.com/a/1190000018250127

  7. React Fiber 原理介绍

感谢你的打赏哦!